/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package jmediaplayer;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
import java.util.logging.*;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import jmediaplayer.events.*;
import org.jaudiotagger.audio.*;
import org.jaudiotagger.audio.exceptions.*;
import org.jaudiotagger.tag.*;

/**
 * A JPlaylist control holds a collection of AudioFile in a collection.
 * JPlaylist control implements some method to navigate through the list of songs.
 * Use PlaylistEvent to interact with other controls.
 * @author PHUOCVIET
 */
public class JPlaylist
        extends JPanel
        implements PropertyChangeListener {

    public static final int MODE_NORMAL = 0;
    public static final int MODE_REPEAT = 1;
    public static final int MODE_SHUFFLE = 2;
    public static final int MODE_REPEAT_SHUFFLE = 3;
    static String PlaylistPath;

    public JPlaylist() {
        super();
        initComponents();
    }

    /**
     * init all components in this panel
     */
    private void initComponents() {
        PlaylistPath = System.getProperty("user.dir");
        setLayout(new java.awt.BorderLayout(5, 5));
        setPreferredSize(new Dimension(JPlayer.PLAYER_WIDTH, 300));

        // Add the command panel at bottom
        commandPanel = new JPanel();
        commandPanel.setName("commandPanel");
        commandPanel.setLayout(new javax.swing.BoxLayout(commandPanel, javax.swing.BoxLayout.LINE_AXIS));
        commandPanel.setBackground(Color.CYAN);
        commandPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        this.add(commandPanel, java.awt.BorderLayout.PAGE_END);

        // Add all buttons to the command panel
        // button to add songs
        btnAdd = new JButton();
        btnAdd.setName("btnAdd");
        btnAdd.setText("+");
        btnAdd.setToolTipText(Constants.PLTIP_ADD);
        btnAdd.addActionListener(new java.awt.event.ActionListener() {

            public void actionPerformed(ActionEvent e) {
                btnAddActionListener(e);
            }
        });
        commandPanel.add(btnAdd);
        commandPanel.add(Box.createHorizontalStrut(5));

        // button to remove some songs
        btnRemove = new JButton();
        btnRemove.setName("btnRemove");
        btnRemove.setText("-");
        btnRemove.setToolTipText(Constants.PLTIP_REMOVE);
        btnRemove.addActionListener(new java.awt.event.ActionListener() {

            public void actionPerformed(ActionEvent e) {
                btnRemoveActionPerformed(e);
            }
        });
        commandPanel.add(btnRemove);
        commandPanel.add(Box.createHorizontalStrut(20));

        // button to open a playlist file
        btnOpen = new JButton();
        btnOpen.setName("btnOpen");
        btnOpen.setText("Import");
        btnOpen.setToolTipText(Constants.PLTIP_IMPORT);
        btnOpen.addActionListener(new java.awt.event.ActionListener() {

            public void actionPerformed(ActionEvent e) {
                btnOpenActionPerformed(e);
            }
        });
        commandPanel.add(btnOpen);
        commandPanel.add(Box.createHorizontalStrut(5));

        // button to save a playlist file
        btnSave = new JButton();
        btnSave.setName("btnSave");
        btnSave.setText("Save Playlist");
        btnSave.setToolTipText(Constants.PLTIP_SAVE);
        btnSave.addActionListener(new java.awt.event.ActionListener() {

            public void actionPerformed(ActionEvent e) {
                btnSaveActionPerformed(e);
            }
        });
        commandPanel.add(btnSave);
        commandPanel.add(Box.createHorizontalStrut(10));

        // progress bar to visualize some task
        progress = new JProgressBar();
        progress.setBorderPainted(true);
        progress.setOpaque(false);
        progress.setPreferredSize(new Dimension(100, 10));
        progress.setSize(new Dimension(100, 10));
        progress.setMaximumSize(new Dimension(100, 10));
        progress.setAlignmentX(LEFT_ALIGNMENT);

        status = new JLabel("Idle");
        status.setAlignmentX(LEFT_ALIGNMENT);
        status.setOpaque(false);

        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        panel.setOpaque(false);
        panel.setToolTipText(Constants.PLTIP_STATUS);
        panel.add(status);
        panel.add(progress);
        commandPanel.add(panel);
        commandPanel.add(Box.createHorizontalStrut(3));

        // Add the song list
        songList = new JList(new DefaultListModel());

        // Add mouse adapter and key listener to the list
        MouseAdapter listener = new ListReorderListener(this);
        songList.addMouseListener(listener);
        songList.addMouseMotionListener(listener);
        PlaylistKeyListener keyListener = new PlaylistKeyListener(this);
        songList.addKeyListener(keyListener);

        // the renderer for the list
        SongListRenderer renderer = new SongListRenderer();
        songList.setCellRenderer(renderer);

        // add list to UI
        scrollSongs = new JScrollPane();
        scrollSongs.setViewportView(songList);
        this.add(scrollSongs, java.awt.BorderLayout.CENTER);
    }

    // <editor-fold defaultstate="expanded" desc="button action listeners">
    /**
     * Add song(s) to playlist
     * @param e
     */
    private void btnAddActionListener(ActionEvent e) {
        // Prepare the FileChooser dialog
        JFileChooser dialog = new JFileChooser(PlaylistPath);
        dialog.setMultiSelectionEnabled(true);
        dialog.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        dialog.setAcceptAllFileFilterUsed(false);
        // Filter files to supported types
        dialog.setFileFilter(new AudioFileFilter(true));

        // Show dialog
        if (dialog.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {

            // execute the task adding songs
            progress.setIndeterminate(true);
            progress.setString("Adding files...");
            AddSongsTask ast = new AddSongsTask(this, dialog, AddSongsTask.MODE_FILE);
            ast.addPropertyChangeListener(this);
            ast.execute();

            // set for the next time dialog's shown
            PlaylistPath = dialog.getCurrentDirectory().getPath();
        }
    }

    /**
     * Remove song(s) from playlist
     * @param e
     */
    private void btnRemoveActionPerformed(ActionEvent e) {
        if (JOptionPane.showConfirmDialog(
                this,
                "Are you sure you want to delete selected file(s) from playlist?",
                "Delete Confirm",
                JOptionPane.OK_CANCEL_OPTION)
                == JOptionPane.OK_OPTION) {
            DefaultListModel dlm = (DefaultListModel) getSongList().getModel();
            Object[] objs = getSongList().getSelectedValues();
            for (int i = 0; i < objs.length; i++) {
                if (objs[i] == getPlayingFile()) {
                    processPlaylistEvent(new PlaylistEvent(this, songList, PlaylistEvent.STOP, 0));
                }
                dlm.removeElement(objs[i]);
                songs.remove(objs[i]);
            }
            // reset shuffle list to reinit it next navigation
            shuffledIndices = null;
        }
    }

    /**
     * Import songs in a playlist file to the list
     * @param e
     */
    private void btnOpenActionPerformed(ActionEvent e) {
        // prepare the dialog
        JFileChooser fileChooser = new JFileChooser(PlaylistPath);
        fileChooser.addChoosableFileFilter(new PlaylistFileFilter());
        fileChooser.setMultiSelectionEnabled(false);

        if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            // execute task adding files
            progress.setIndeterminate(true);
            progress.setString("Importing playlist...");
            AddSongsTask ast = new AddSongsTask(this, fileChooser, AddSongsTask.MODE_SONGLIST);
            ast.addPropertyChangeListener(this);
            ast.execute();
        }
    }

    /**
     * Save song list to a .m3u file
     * @param e
     */
    private void btnSaveActionPerformed(ActionEvent e) {
        // prepare dialog
        JFileChooser fileChooser = new JFileChooser(PlaylistPath);
        fileChooser.addChoosableFileFilter(new PlaylistFileFilter());
        fileChooser.setMultiSelectionEnabled(false);

        if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
            // set for the next navigation
            PlaylistPath = fileChooser.getCurrentDirectory().getPath();

            File file = fileChooser.getSelectedFile();
            String path = file.getParent();
            String name = file.getName();
            if (!name.endsWith(".m3u")) {
                name += ".m3u";
            }

            file = new File(path, name);
            savePlaylist(file);
        }
    }

    /**
     * Write the list of JSongList to file
     * @param file
     */
    private void savePlaylist(File file) {
        try {
            if (songs != null) {
                songs.writeToFile(file);
            }
        } catch (IOException ex) {
            Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    // </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Playlist event implementation">
    private ArrayList<PlaylistListener> playlistListenerList;

    /** Register an action event listener */
    public synchronized void addPlaylistListener(PlaylistListener listener) {
        if (playlistListenerList == null) {
            playlistListenerList = new ArrayList<PlaylistListener>(2);
        }
        if (!playlistListenerList.contains(listener)) {
            playlistListenerList.add(listener);
        }
    }

    /** Remove an action event listener */
    public synchronized void removePlaylistListener(PlaylistListener listener) {
        if (playlistListenerList != null && playlistListenerList.contains(listener)) {
            playlistListenerList.remove(listener);
        }
    }

    /** Process all added PlaylistListener */
    void processPlaylistEvent(PlaylistEvent e) {
        ArrayList list;
        synchronized (this) {
            if (playlistListenerList == null) {
                return;
            }
            list = (ArrayList) playlistListenerList.clone();
        }

        for (int i = 0; i < list.size(); i++) {
            PlaylistListener listener = (PlaylistListener) list.get(i);
            listener.PlaylistActionPerformed(e);
        }
    }
    // <editor-fold>

    // <editor-fold defaultstate="expanded" desc="public methods">
    /**
     * Scroll the playlist to the playing song item
     */
    public void scrollToPlayingSong() {
        int index = this.getPlayingIndex();
        Rectangle bound = songList.getCellBounds(index, index);
        if (bound != null) {
            songList.scrollRectToVisible(bound);
        }
    }

    /**
     * Move to next song in playlist corresponding to the play mode
     * @param PlayingMode is the mode of playing, use JPlayer.MODE_XXXX
     */
    public void navigateNext(int playingMode, boolean auto) {
        switch (playingMode) {
            case JPlaylist.MODE_NORMAL: {
                setPlayingIndex(playingIndex + 1);
                if (playingIndex < 0 || playingIndex >= getSongList().getModel().getSize()) {
                    processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.STOP, playingIndex));
                } else {
                    processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
                }
            }
            break;
            case JPlaylist.MODE_REPEAT: {
                setPlayingIndex(playingIndex + 1);
                if (playingIndex >= getSongList().getModel().getSize()) {
                    setPlayingIndex(0);
                }
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
            case JPlaylist.MODE_SHUFFLE: { //TODO something not good if songlist change
                if (shuffledIndices == null) {
                    initShuffle(getSongList().getModel().getSize());
                }
                int index = shuffledIndices.indexOf(playingIndex) + 1;
                if (index >= getSongList().getModel().getSize()) {
                    if (auto) {
                        processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.STOP, 0));
                    } else {
                        index = 0;
                    }
                }
                setPlayingIndex(shuffledIndices.get(index));
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
            case JPlaylist.MODE_REPEAT_SHUFFLE: {
                Random rand = new Random();
                if (!isForwarding) {
                    setPlayingIndex(lastIndex);
                    isForwarding = true;
                } else {
                    lastIndex = playingIndex;
                    int index;
                    do {
                        index = rand.nextInt(getSongList().getModel().getSize());
                    } while (getSongList().getModel().getSize() > 1 && index == playingIndex);
                    setPlayingIndex(index);
                }
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
        }
        scrollToPlayingSong();
    }

    /**
     * Move to previous song in playlist corresponding to play mode
     * @param PlayingMode is the mode of playing, use JPlayer.MODE_XXXX
     */
    public void navigatePrevious(int playingMode) {
        switch (playingMode) {
            case JPlaylist.MODE_NORMAL: {
                setPlayingIndex(playingIndex - 1);
                if (playingIndex < 0 || playingIndex >= getSongList().getModel().getSize()) {
                    processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.STOP, playingIndex));
                } else {
                    processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
                }
            }
            break;
            case JPlaylist.MODE_REPEAT: {
                setPlayingIndex(playingIndex - 1);
                if (playingIndex < 0) {
                    setPlayingIndex(getSongList().getModel().getSize() - 1);
                }
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
            case JPlaylist.MODE_SHUFFLE: {
                if (shuffledIndices == null) {
                    initShuffle(getSongList().getModel().getSize());
                }
                int index = shuffledIndices.indexOf(playingIndex) - 1;
                if (index < 0) {
                    index = getSongList().getModel().getSize() - 1;
                }
                setPlayingIndex(shuffledIndices.get(index));
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
            case JPlaylist.MODE_REPEAT_SHUFFLE: {
                Random rand = new Random();
                if (isForwarding) {
                    setPlayingIndex(lastIndex);
                    isForwarding = false;
                } else {
                    lastIndex = playingIndex;
                    int index;
                    do {
                        index = rand.nextInt(getSongList().getModel().getSize());
                    } while (getSongList().getModel().getSize() > 1 && index == playingIndex);
                    setPlayingIndex(index);
                }
                processPlaylistEvent(new PlaylistEvent(this, getSongList(), PlaylistEvent.PLAY, playingIndex));
            }
            break;
        }
        scrollToPlayingSong();
    }

    /**
     * Prepare the shuffledIndices for navigating in Shuffle mode
     * @param size size of the song list
     */
    public void initShuffle(int size) {
        if (size <= 0) {
            shuffledIndices = new ArrayList<Integer>();
            return;
        }
        shuffledIndices = new ArrayList<Integer>(size);
        if (size == 1) {
            shuffledIndices.add(0, 0);
            return;
        }

        Random rand = new Random();
        int t, temp;
        for (int i = 0; i < size; i++) {
            shuffledIndices.add(i);
        }
        for (int i = 0; i < size; i++) {
            do {
                t = rand.nextInt(size);
            } while (size > 1 && t == i);
            temp = shuffledIndices.get(t);
            shuffledIndices.set(t, shuffledIndices.get(i));
            shuffledIndices.set(i, temp);
        }
    }

    /**
     * Get title of an AudioFie to display
     * if file doesn't have tag then use it's file name
     * @param file
     * @return the title
     */
    public static String getTagTitle(AudioFile file) {
        Tag tag = file.getTag();
        String title;
        // if file have no tag, use the file name for title
        if (tag == null) {
            title = file.getFile().getName();
        } else {
            // add song title
            String s_Title = tag.getFirst(FieldKey.TITLE);
            // Check if title is empty or contain only space
            boolean goodTitle = false;
            for (int i = 0; i < s_Title.length() && !goodTitle; i++) {
                if (s_Title.charAt(i) != ' ') {
                    goodTitle = true;
                }
            }

            if (goodTitle) {
                title = s_Title;
            } else { // use file name if title in tag empty
                title = file.getFile().getName();
            }
        }
        return title;
    }

    public void propertyChange(PropertyChangeEvent evt) {
        String prop = evt.getPropertyName();

        if (prop.equals("task")) {
//            progress.setString(evt.getNewValue().toString());
            status.setText(evt.getNewValue().toString());
        } else if (prop.equals("done")) {
            progress.setIndeterminate(false);
//            progress.setString("Idle");
            status.setText("Idle");
            songList.updateUI();
            scrollToPlayingSong();
            // reset shuffle list to reinit it next navigation
            shuffledIndices = null;
        }
    }
// </editor-fold>
//
// UI components
    private JPanel commandPanel;
    private JButton btnAdd;
    private JButton btnRemove;
    private JButton btnOpen;
    private JButton btnSave;
    private JScrollPane scrollSongs;
    private JList songList;
    JSongList songs = null;
    private int playingIndex = -1;
    private AudioFile playingFile = null;
    private Color playingHightlight = Color.blue;
    private ArrayList<Integer> shuffledIndices = null;
    private int lastIndex = 0;
    private boolean isForwarding = true;
    private JProgressBar progress;
    private JLabel status;

    // <editor-fold defaultstate="collapsed" desc="get/set">
    /**
     * @return the songList
     */
    public JList getSongList() {
        return songList;
    }

    /**
     * @param songList the songList to set
     */
    public void setSongList(JList songList) {
        this.songList = songList;
    }

    /**
     * @return the playingIndex
     */
    public int getPlayingIndex() {
        return playingIndex;
    }

    /**
     * Set the playing file index
     * Keep actual value in save range [-1 .. n]
     * @param playingIndex the playingIndex to set
     */
    public void setPlayingIndex(int playingIndex) {
        DefaultListModel dlm = (DefaultListModel) getSongList().getModel();
        if (playingIndex < 0) {
            playingIndex = -1;
//            playingFile = null;
        } else if (playingIndex >= dlm.size()) {
            playingIndex = dlm.size();
//            playingFile = null;
        } else {
            playingFile = (AudioFile) dlm.get(playingIndex);
        }
        this.playingIndex = playingIndex;
        songList.updateUI();
    }

    /**
     * @return the playingHightlight
     */
    public Color getPlayingHightlight() {
        return playingHightlight;
    }

    /**
     * @param playingHightlight the playingHightlight to set
     */
    public void setPlayingHightlight(Color playingHightlight) {
        this.playingHightlight = playingHightlight;
    }

    /**
     * Get the file marked for play
     * @return the mark file
     */
    public AudioFile getPlayingFile() {
        DefaultListModel dlm = (DefaultListModel) getSongList().getModel();
        if (playingIndex >= 0 && playingIndex < dlm.getSize()) {
            return (AudioFile) dlm.get(playingIndex);
        }
        return null;
    }
    // </editor-fold>

    /**
     * Class SongListRenderer to render cells for song list
     * @author PHUOCVIET
     */
    class SongListRenderer extends JPanel implements ListCellRenderer {

        public Component getListCellRendererComponent(
                JList list,
                Object value,
                int index,
                boolean isSelected,
                boolean cellHasFocus) {
            JLabel title = new JLabel();
            JLabel duration = new JLabel();
            AudioFile file = new AudioFile();
            Tag tag = file.getTag();

            this.setLayout(new java.awt.BorderLayout(5, 5));
            if (value instanceof AudioFile) {
                file = (AudioFile) value;
                tag = file.getTag();
                this.removeAll();

                title = new JLabel(JPlaylist.getTagTitle(file));

                this.add(title, java.awt.BorderLayout.CENTER);

                // prepare the timespan string
                int time = file.getAudioHeader().getTrackLength();
                String s = translateIntToTime(time);

                // add song duration
                duration = new JLabel(s);
                this.add(duration, java.awt.BorderLayout.LINE_END);

                if (tag != null) {
                    this.setToolTipText("[Artist]: " + tag.getFirst(FieldKey.ARTIST)
                            + "\n |%| [Album]:" + tag.getFirst(FieldKey.ALBUM));
                }
            }

            if (isSelected) {
                setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
                setBackground(list.getSelectionBackground());
                Color foreground = list.getSelectionForeground();
                setForeground(foreground);
                title.setForeground(foreground);
                duration.setForeground(foreground);
            }
            if (cellHasFocus) {
                setBorder(javax.swing.BorderFactory.createLineBorder(Color.orange));
            }
            if (!(isSelected || cellHasFocus)) {
                setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
                setBackground(list.getBackground());
                Color foreground = list.getForeground();
                setForeground(foreground);
                title.setForeground(foreground);
                duration.setForeground(foreground);
            }
            if (value == playingFile) {
                title.setForeground(playingHightlight);
                duration.setForeground(playingHightlight);
            }

            return this;
        }

        /**
         * Generate a string represent a timespan of a number of seconds
         * @param in number of seconds
         */
        private String translateIntToTime(int in) {
            String result = "";
            // second
            String t = Integer.toString(in % 60);
            if (t.length() < 2) {
                t = "0" + t;
            }
            result = ":" + t;

            // minute
            in /= 60;
            t = Integer.toString(in % 60);
            if (t.length() < 2) {
                t = "0" + t;
            }
            result = t + result;

            // hour
            in /= 60;
            if (in > 0) {
                t = Integer.toString(in);
                if (t.length() < 2) {
                    t = "0" + t;
                }
                result = t + ":" + result;
            }
            return result;
        }
    }
}

/**
 * Handle command using keyboard in playlist:
 *  -
 * @author PHUOCVIET
 */
class PlaylistKeyListener extends KeyAdapter {

    private JPlaylist list;

    public PlaylistKeyListener(JPlaylist list) {
        if (list == null) {
            throw new IllegalArgumentException("Playlist must not be null");
        }
        if (!(list.getSongList().getModel() instanceof DefaultListModel)) {
            throw new IllegalArgumentException("Song list must have a DefaultListModel");
        }
        this.list = list;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_DELETE: {
                if (JOptionPane.showConfirmDialog(this.list, "Are you sure you want to delete selected file(s) from playlist?", "Delete Confirm", JOptionPane.OK_CANCEL_OPTION)
                        == JOptionPane.OK_OPTION) {
                    DefaultListModel dlm = (DefaultListModel) list.getSongList().getModel();
                    Object[] objs = list.getSongList().getSelectedValues();
                    for (int i = 0; i < objs.length; i++) {
                        if (objs[i] == list.getPlayingFile()) {
                            list.processPlaylistEvent(new PlaylistEvent(list, list.getSongList(), PlaylistEvent.STOP, 0));
                        }
                        dlm.removeElement(objs[i]);
                        list.songs.remove(objs[i]);
                    }
                }
            }
            break;
            case (KeyEvent.VK_ENTER): {
                int index = list.getSongList().getLeadSelectionIndex();
                DefaultListModel listModel = (DefaultListModel) list.getSongList().getModel();
                if (index >= 0 && index < listModel.size()) {
                    list.processPlaylistEvent(new PlaylistEvent(list, list.getSongList(), PlaylistEvent.PLAY, index));
                }
            }
            break;
        }
    }
}

/**
 * Mouse Adapter for handle when drag to sort items in JList
 *  also sort corresponding items in JSongList
 * Handle the playing command when double-click 
 *  with left mouse button on an item
 * @author PHUOCVIET
 */
class ListReorderListener extends MouseAdapter {

    private JPlaylist list;
    private int pressIndex = 0;
    private int releaseIndex = 0;

    public ListReorderListener(JPlaylist list) {
        if (list == null) {
            throw new IllegalArgumentException("Playlist must not be null");
        }
        if (!(list.getSongList().getModel() instanceof DefaultListModel)) {
            throw new IllegalArgumentException("Song list must have a DefaultListModel");
        }
        this.list = list;
    }

    @Override
    public void mousePressed(MouseEvent e) {
        if (e.getButton() == MouseEvent.BUTTON1) {
            pressIndex = list.getSongList().locationToIndex(e.getPoint());
        }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        if (e.getModifiers() == MouseEvent.BUTTON1_MASK) {
            releaseIndex = list.getSongList().locationToIndex(e.getPoint());
            if (releaseIndex != pressIndex && releaseIndex != -1) {
                reorder();
            }
        }
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        mouseReleased(e);
        pressIndex = releaseIndex;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getClickCount() == 2) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                int index = list.getSongList().locationToIndex(e.getPoint());
                DefaultListModel dlm = null;
                if (list.getSongList().getModel() instanceof DefaultListModel) {
                    dlm = (DefaultListModel) list.getSongList().getModel();
                }
                if (dlm != null) {
                    list.setPlayingIndex(index);
                    list.processPlaylistEvent(new PlaylistEvent(list, list.getSongList(), PlaylistEvent.PLAY, index));
                }
            }
        }
    }

    private void reorder() {
        DefaultListModel model = (DefaultListModel) list.getSongList().getModel();
        Object dragee = model.elementAt(pressIndex);
        model.removeElementAt(pressIndex);
        model.insertElementAt(dragee, releaseIndex);
        list.songs.remove(pressIndex);
        list.songs.add(releaseIndex, dragee);
    }
}

class PlaylistFileFilter extends FileFilter {

    @Override
    public boolean accept(File f) {
        if (f.isDirectory()) {
            return true;
        }
        String s = f.getName().toLowerCase();
        if (s.endsWith(".m3u")) {
            return true;
        }
        return false;
    }

    @Override
    public String getDescription() {
        return "Playlist Files";
    }
}

/**
 * A long running task to add songs to playlist.
 * It has two type of task:
 *  - Add song chosen in a dialog,
 *    if choosing a folder it'll fetch all folder content recursively.
 *  - Add song from a selected .m3u file from dialog.
 * @author PHUOCVIET
 */
class AddSongsTask extends SwingWorker<Void, Void> {

    public static final int MODE_FILE = 1;
    public static final int MODE_SONGLIST = 2;
    private JPlaylist playlist;
    private int mode;
    private JFileChooser dialog;
    private String task;
    private long lastTime;
    private DefaultListModel listModel;

    public AddSongsTask(JPlaylist playlist, JFileChooser dialog, int mode) {
        this.playlist = playlist;
        this.mode = mode;
        this.dialog = dialog;
    }

    @Override
    protected void done() {
        firePropertyChange("done", null, null);
    }

    @Override
    protected Void doInBackground() {
        lastTime = System.currentTimeMillis();
        listModel = (DefaultListModel) playlist.getSongList().getModel();
        if (playlist.songs == null) {
            playlist.songs = new JSongList();
        }

        switch (mode) {
            case MODE_FILE: {
                File[] files = dialog.getSelectedFiles();
                AudioFile file = null;

                for (File f : files) {
                    try {
                        if (f.isFile()) {
                            file = AudioFileIO.read(f);
                            if (file != null) {
                                addSong(file);
                            }
                        } else if (f.isDirectory()) {
                            loadFromDirectory(f, true);
                        }
                    } catch (CannotReadException ex) {
                        Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IOException ex) {
                        Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (TagException ex) {
                        Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (ReadOnlyFileException ex) {
                        Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (InvalidAudioFrameException ex) {
                        Logger.getLogger(JPlaylist.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
            break;
            case MODE_SONGLIST: {
                try {
                    loadFromFile(dialog.getSelectedFile());
                } catch (PlaylistException ex) {
                    JOptionPane.showMessageDialog(playlist, ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
                } catch (FileNotFoundException ex) {
                    JOptionPane.showMessageDialog(playlist, ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
                } catch (IOException ex) {
                    JOptionPane.showMessageDialog(playlist, ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
                }
            }
            break;
        }
        return null;
    }

    /**
     * Add song to the list with checking if exists
     * @param file file to add
     * @return added or not
     */
    private boolean addSong(AudioFile file) {
        boolean toAdd = true;
        if (listModel == null) {
            return false;
        } else {
            for (int j = 0; j < listModel.size() && toAdd; j++) {
                if (((AudioFile) listModel.get(j)).getFile().getPath().equals(file.getFile().getPath())) {
                    toAdd = false;
                }
            }
            if (toAdd) {
                long last = System.currentTimeMillis();
                if (last - lastTime >= 100) {
                    setTask("Adding file: " + file.getFile().getName());
                    lastTime = last;
                }
                listModel.addElement(file);
                playlist.songs.add(file);
            } else {
                JOptionPane.showMessageDialog(playlist, "File " + file.getFile().getPath() + " already exists in playlist!", "File exists in playlist", JOptionPane.ERROR_MESSAGE);
            }
        }
        return toAdd;
    }

    private void loadFromDirectory(File dir, boolean recurse) {
        if (dir.isDirectory()) {
            File[] files = dir.listFiles(new AudioFileFilter(true));
            AudioFile file = null;
            AudioFileFilter aff = new AudioFileFilter(true);

            for (int i = 0; i < files.length; i++) {
                if (files[i].isFile() && aff.accept(files[i])) {
                    try {
                        file = AudioFileIO.read(files[i]);
                        if (file != null) {
                            this.addSong(file);
                        }
                    } catch (Exception e) {
                        // Do nothing.  Bad file, don't add.
                    }
                } else if (files[i].isDirectory() && recurse) {
                    loadFromDirectory(files[i], recurse);
                }
            }
        }
    }

    private void loadFromFile(File m3uFile)
            throws PlaylistException, FileNotFoundException, IOException {

        FileReader in = new FileReader(m3uFile);
        BufferedReader reader = new BufferedReader(in);
        String str = reader.readLine();
        AudioFile file = null;

        if (!str.equals("#EXTM3U\r\n")) {
            while ((str = reader.readLine()) != null) {
                if (str.indexOf("EXTINF:") != -1) {
                    str = reader.readLine();

                    try {
                        File f = new File(str);
                        file = AudioFileIO.read(f);
                        addSong(file);
                    } catch (Exception e) {
                        // Do nothing, just don't add it
                    }
                } else {
                    throw new PlaylistException();
                }
            }
        } else {
            throw new PlaylistException();
        }

        reader.close();
        in.close();
    }

    /**
     * @return the task
     */
    public String getTask() {
        return task;
    }

    /**
     * @param task the task to set
     */
    public void setTask(String task) {
        firePropertyChange("task", this.task, task);
        this.task = task;
    }
}
